In [1]:
import lime
import sklearn
import numpy as np
import sklearn
import sklearn.ensemble
import sklearn.metrics

In the previous tutorial, we looked at lime in the two class case. In this tutorial, we will use the 20 newsgroups dataset again, but this time using all of the classes.

In [2]:
from sklearn.datasets import fetch_20newsgroups
newsgroups_train = fetch_20newsgroups(subset='train')
newsgroups_test = fetch_20newsgroups(subset='test')
# making class names shorter
class_names = [x.split('.')[-1] if 'misc' not in x else '.'.join(x.split('.')[-2:]) for x in newsgroups_train.target_names]
In [3]:
print ','.join(class_names)
atheism,graphics,ms-windows.misc,hardware,hardware,x,misc.forsale,autos,motorcycles,baseball,hockey,crypt,electronics,med,space,christian,guns,mideast,politics.misc,religion.misc

Again, let's use the tfidf vectorizer, commonly used for text.

In [4]:
vectorizer = sklearn.feature_extraction.text.TfidfVectorizer(lowercase=False)
train_vectors = vectorizer.fit_transform(newsgroups_train.data)
test_vectors = vectorizer.transform(newsgroups_test.data)

This time we will use Multinomial Naive Bayes for classification, so that we can make reference to this document.

In [5]:
from sklearn.naive_bayes import MultinomialNB
c = MultinomialNB(alpha=.01)
c.fit(train_vectors, newsgroups_train.target)
Out[5]:
MultinomialNB(alpha=0.01, class_prior=None, fit_prior=True)
In [6]:
pred = c.predict(test_vectors)
sklearn.metrics.f1_score(newsgroups_test.target, pred, average='weighted')
Out[6]:
0.83501841939981736

We see that this classifier achieves a very high F score. The sklearn guide to 20 newsgroups indicates that Multinomial Naive Bayes overfits this dataset by learning irrelevant stuff, such as headers, by looking at the features with highest coefficients for the model in general. We now use lime to explain individual predictions instead.

In [7]:
from lime import lime_text
explainer = lime_text.LimeTextExplainer(vocabulary=vectorizer.vocabulary_, class_names=class_names)

Previously, we used the default parameter for label when generating explanation, which works well in the binary case.
For the multiclass case, we have to determine for which labels we will get explanations, via the 'labels' parameter.
Below, we generate explanations for labels 1,2,3, 5 and 14.

In [8]:
idx = 1340
exp = explainer.explain_instance(test_vectors[idx], c.predict_proba, num_features=6, labels=[1,2,3,5,14])
print 'Document id: %d' % idx
print 'Predicted class =', class_names[c.predict(test_vectors[idx])]
print 'True class: %s' % class_names[newsgroups_test.target[idx]]
Document id: 1340
Predicted class = atheism
True class: atheism

Now, we can see the explanations for different labels. Notice that the positive and negative signs are with respect to a particular label - so that words that are negative towards class 1 may be positive towards class 14.

In [9]:
print 'Explanation for class %s' % class_names[1]
print '\n'.join(map(str, exp.as_list(label=1)))
print
print 'Explanation for class %s' % class_names[14]
print '\n'.join(map(str, exp.as_list(label=14)))
Explanation for class graphics
(u'Rice', -0.0019224199171263556)
(u'fsu', -0.0017086769141540839)
(u'Theism', -0.0016988830737923496)
(u'Genocide', -0.0016929280629683521)
(u'Jews', -0.0016286395918915096)
(u'the', -0.0015462600143028348)

Explanation for class space
(u'Rice', -0.0027187689647229243)
(u'Jews', -0.0023097707936882688)
(u'Theism', -0.0022857497551192932)
(u'Genocide', -0.0022657732728714993)
(u'the', -0.0018481556347905224)
(u'fsu', -0.0017589605272295664)

Another alternative is to ask LIME to generate labels for the top $K$ classes. This is shown below with $K=5$.
To see which labels have explanations, use the available_labels function.

In [10]:
exp = explainer.explain_instance(test_vectors[idx], c.predict_proba, num_features=6, top_labels=5)
print exp.available_labels()
[0, 15, 19, 17, 16]

Now let's see some the explanation for the top class, with the associated text.

In [11]:
from IPython.core.display import display, HTML
print 'Explaining class atheism'
display(HTML(exp.as_html(text=newsgroups_test.data[idx], label=0, include=['predict_proba', 'pos', 'neg', 'local'])))
Explaining class atheism

Text with highlighted words

From: conor@owlnet.rice.edu (Conor Frederick Prischmann)
Subject: Re: Genocide is Caused by Theism : Evidence?
Organization: Rice University
Lines: 23

In article <C60A0s.DvI@mailer.cc.fsu.edu> dekorte@dirac.scri.fsu.edu (Stephen L. DeKorte) writes:
>
>I saw a 3 hour show on PBS the other day about the history of the
>Jews. Appearently, the Cursades(a religious war agianst the muslilams
>in 'the holy land') sparked the widespread persecution of muslilams
>and jews in europe. Among the supporters of the persiecution, were none
>other than Martin Luther, and the Vatican.
>
>Later, Hitler would use Luthers writings to justify his own treatment
>of the jews.
>> Genocide is Caused by Theism : Evidence?

Heck, I remember reading a quote of Luther as something like: "Jews should
be shot like deer." And of course much Catholic doctrine for centuries was
extremely anti-Semitic.



--
"Are you so sure that your truth and your justice are worth more than the
truths and justices of other centuries?" - Simone de Beauvoir
"Where is there a certainty that rises above all doubt and withstands all
critique?" - Karl Jaspers Rice University, Will Rice College '96

We notice that the classifier is using reasonable words (such as 'Theism', 'Semitic', etc), as well as unreasonable ones ('Rice', 'owlnet').

Let's see the explanation for 'atheism', 'christian', and 'mideast', without the associated text

In [12]:
print 'Explaining class atheism'
display(HTML(exp.as_html(label=0)))
print
print 'Explaining class christian'
display(HTML(exp.as_html(label=15)))
print
print 'Explaining class mideast'
display(HTML(exp.as_html(label=17)))
Explaining class atheism


Explaining class christian


Explaining class mideast

We notice that looking at the explanations for different classes can lead to different insights.
Finally, we follow the suggestion of removing headers, footers and quotes, and explain the same example with the new data.

In [13]:
newsgroups_train = fetch_20newsgroups(subset='train', remove=('headers', 'footers', 'quotes'))
newsgroups_test = fetch_20newsgroups(subset='test',remove=('headers', 'footers', 'quotes'))
train_vectors = vectorizer.fit_transform(newsgroups_train.data)
test_vectors = vectorizer.transform(newsgroups_test.data)
c = MultinomialNB(alpha=.01)
c.fit(train_vectors, newsgroups_train.target)
explainer = lime_text.LimeTextExplainer(vocabulary=vectorizer.vocabulary_, class_names=class_names)
In [14]:
exp = explainer.explain_instance(test_vectors[idx], c.predict_proba, num_features=6, top_labels=5)
print exp.available_labels()
[15, 17, 0, 16, 19]

Notice how different the explanations are for the classifier without headers, footers and quotes. The prediction changes, but so do the reasons.

In [15]:
print 'Explaining class atheism'
display(HTML(exp.as_html(label=0)))
print
print 'Explaining class christian'
display(HTML(exp.as_html(label=15)))
print
print 'Explaining class mideast'
display(HTML(exp.as_html(label=17)))
Explaining class atheism


Explaining class christian


Explaining class mideast

Let's see the explanation with the text for the top class (christian):

In [16]:
from IPython.core.display import display, HTML
print 'Explaining class christian'
display(HTML(exp.as_html(text=newsgroups_test.data[idx], label=15, include=['predict_proba', 'pos', 'neg', 'local'])))
Explaining class christian

Text with highlighted words


Heck, I remember reading a quote of Luther as something like: "Jews should
be shot like deer." And of course much Catholic doctrine for centuries was
extremely anti-Semitic.


Notice how short the text became after removing all of that information. One begins to wonder if this version of the dataset is still useful, or if it is better to find another dataset altogether.
Anyway, I hope this illustrated how to use LIME to explain arbitrary classifiers in the multiclass case!